package com.softaria.windows.logic;

import java.util.HashSet;
import java.util.List;
import java.util.Set;

import com.google.gwt.event.dom.client.ClickEvent;
import com.google.gwt.event.dom.client.ClickHandler;
import com.google.gwt.event.logical.shared.SelectionEvent;
import com.google.gwt.event.logical.shared.SelectionHandler;
import com.google.gwt.event.logical.shared.ValueChangeEvent;
import com.google.gwt.event.logical.shared.ValueChangeHandler;
import com.google.gwt.user.client.Command;
import com.google.gwt.user.client.History;
import com.google.gwt.user.client.ui.AbsolutePanel;
import com.google.gwt.user.client.ui.TreeItem;
import com.softaria.gwt.common.client.ResizeManager;
import com.softaria.windows.config.Guard;
import com.softaria.windows.config.MenuConfiguration;
import com.softaria.windows.factories.BadUrlException;
import com.softaria.windows.factories.CloseConfirmation;
import com.softaria.windows.factories.CloseListenerRegistrator;
import com.softaria.windows.factories.PerspectiveFactory;
import com.softaria.windows.factories.ViewFactory;
import com.softaria.windows.model.ModelUrl;
import com.softaria.windows.model.Perspective;
import com.softaria.windows.model.PerspectiveUrl;
import com.softaria.windows.model.PositionCalculator;
import com.softaria.windows.model.SizeCalculator;
import com.softaria.windows.model.ViewUrl;
import com.softaria.windows.widgets.Desktop;
import com.softaria.windows.widgets.FrameActivationListener;
import com.softaria.windows.widgets.FrameButtonPressedListener;
import com.softaria.windows.widgets.ResizeListenerRegistrator;
import com.softaria.windows.widgets.TabbedWorkPanel;

/**
 * WindowsManager is main logic object. It is also the only public class in
 * �logic� package. It provides API for controlling the system. Other objects (
 * {@link ViewManager}, {@link PerspectiveManager} and {@link HelpManager}) are
 * used by WindowManager.
 * 
 * As our system is URL driven, main entry point of WindowsManager is the method
 * onHistoryChanged which takes URL, parses it and then open perspective or
 * view, according to url. As you probably know, GWT module has fixed URL, which
 * does not changed during the application work, so the only chance to create
 * URL driven system in GWT is to use anchors.
 * 
 * Anchors not only can change, but GWT also provides special History API for
 * manipulating them and listening for their changes. So, WindowsManager listens
 * to anchor change.
 * 
 * And it understands the following URL format:
 * 
 * To open perspective with id perspectiveID:
 * http://someserver.com/SomeGwtModule.html?someParameter=someValues
 * #perspectiveID
 * 
 * To open view with id viewID and parameters vparam1=vvalue1&vparam2=vvalue2 on
 * perspective with id perspectiveID : http://
 * someserver.com/SomeGwtModule.html?someParameter=someValues#perspectiveID
 * /viewID?vparam1=vvalue1&vparam2=vvalue2
 * 
 * It is important that all calls that open or activate view or perspective
 * always go through this entry point. When you types URL in browser it runs
 * automatically. When you call openPerspectiveOrView programmatically, it
 * forces browser URL to be changed correspondingly. It makes URL always
 * corresponds to content. So, you are free to send URLs to your friend and also
 * free to use browser�s �back� button and it will work.
 * 
 * 
 * @author Roman M Kuzmin (roman@softaria.com)
 * 
 */
public class WindowsManager implements ValueChangeHandler<String> {

  private PerspectiveManager perspectiveManager;
  private ViewManager viewManager;
  private HelpManager helpManager;
  private MaximizedFramesResizeManager maxFrameResizeManager;
  private ResizeManager resizeManager;

  private Guard guard;

  private MenuConfiguration config;

  private Desktop desktop;

  /**
   * 
   * @param conf
   * @param desktop
   * @param viewFactory
   * @param guard
   *          - can be null, when no {@link Guard} is required
   */
  public WindowsManager(final MenuConfiguration conf, final Desktop desktop,
      final PerspectiveFactory perspectiveFactory, final ViewFactory viewFactory, final Guard guard) {
    desktop.waitForInitialization(new Command() {

      @Override
      public void execute() {
        initialize(conf, desktop, perspectiveFactory, viewFactory, guard);

      }

    });

  }

  /**
   * Constructor for backward compatibility. It uses primitive
   * PerspectiveFactory, which emulates old straightforward behavior
   * 
   * @param conf
   * @param desktop
   * @param viewFactory
   * @param guard
   */
  public WindowsManager(final MenuConfiguration conf, Desktop desktop, ViewFactory viewFactory,
      Guard guard) {
    this(conf, desktop, new PerspectiveFactory() {

      @Override
      public Perspective create(ModelUrl url, CloseListenerRegistrator closeListenerRegistrator,
          ResizeListenerRegistrator frlr, HelpWriter helpWriter) throws BadUrlException {
        Perspective perspective = new Perspective();

        perspective.setPerspectiveUrl(url.asPerspectiveUrl());
        String label = conf.getMenuLabel(url);
        perspective.setName(label == null ? "" : label);

        return perspective;
      }

    }, viewFactory, guard);
  }

  private void initialize(MenuConfiguration conf, Desktop desktop,
      PerspectiveFactory perspectiveFactory, ViewFactory factory, Guard guard) {
    this.config = conf;
    this.guard = guard;
    this.desktop = desktop;
    this.helpManager = new HelpManager(desktop.getHelpPanel());

    this.maxFrameResizeManager = new MaximizedFramesResizeManager();
    this.resizeManager = new ResizeManager();
    resizeManager.addWindowResizeListener(maxFrameResizeManager);

    initializeHelpManager(helpManager, config);

    this.perspectiveManager = new PerspectiveManager(perspectiveFactory, desktop
        .getTabbedWorkPanel());
    this.viewManager = new ViewManager(factory, maxFrameResizeManager);

    // Initiating left menu
    desktop.getMenuTree().addSelectionHandler(new SelectionHandler<TreeItem>() {

      public void onSelection(SelectionEvent<TreeItem> event) {
        ModelUrl url = (ModelUrl) event.getSelectedItem().getUserObject();
        openPerspectiveOrView(url);

      }

    });

    History.addValueChangeHandler(this);

    String token = History.getToken();

    if ((token != null) && (token.trim().length() > 0)) {
      onHistoryChanged(token);
    }

    if ((perspectiveManager.getOpenedPerspectives().size() == 0)
        && (config.getDefaultPerspective() != null)) {
      openPerspectiveOrView(config.getDefaultPerspective());
    }
  }

  private void initializeHelpManager(HelpManager helpManager, MenuConfiguration config) {
    for (PerspectiveUrl pUrl : config.getPerspecives()) {
      String msg = config.getDefaultHelpMessage(pUrl);
      String title = config.getDefaultHelpTitle(pUrl);
      if (msg != null) {
        helpManager.setHelpMessage(pUrl, title, msg);
      }

      for (ViewUrl vUrl : config.getViews(pUrl)) {
        String vMsg = config.getDefaultHelpMessage(vUrl);
        String vTtl = config.getDefaultHelpTitle(vUrl);
        if (vMsg != null) {
          helpManager.setHelpMessage(vUrl, vTtl, vMsg);
        }
      }

    }

  }

  /**
   * In GWT 1.6 this is the method called by History when URL's anchor is
   * changed
   */
  public void onValueChange(ValueChangeEvent<String> event) {
    onHistoryChanged(event.getValue());
  }

  /**
   * Main entry point for ALL calls
   * 
   * It parses URL and opens corresponding view or perspective
   * 
   * @param historyToken
   *          - part of URL after anchor
   */
  private void onHistoryChanged(String historyToken) {

    if (!checkUserPermissions(historyToken)) {
      return;
    }

    final ModelUrl url = URLParser.parse(historyToken);

    if (url != null) {

      if (url.isView()) {
        // open this view only
        final ViewUrl viewUrl = url.asViewUrl();
        try {
          Perspective perspective = perspectiveManager.openPerspectiveTab(viewUrl
              .getPerspectiveUrl(), new ClickHandler() {

            public void onClick(ClickEvent ev) {
              closePerspective(viewUrl.getPerspectiveUrl());
            }

          }, new Command() {
            public void execute() {
              openPerspectiveOrView(viewUrl.getPerspectiveUrl());
            }
          }, null, helpManager);

          PositionCalculator pc = config.getDefaultPosition(viewUrl);

          SizeCalculator fsize = config.getDefaultSize(viewUrl);

          try {
            openOrActivateView(perspective, viewUrl, pc, fsize, false);
            desktop.getMenuTree().highlight(viewUrl);
            helpManager.setActiveAuthor(url);
          } catch (BadUrlException e) {
            e.printStackTrace();
          }

          updateMaximizedFrames(perspective);
        } catch (BadUrlException e) {
          e.printStackTrace();
        }

      } else {
        try {
          Perspective perspective = perspectiveManager.openPerspectiveTab(url.asPerspectiveUrl(),
              new ClickHandler() {

                public void onClick(ClickEvent ev) {
                  closePerspective(url.asPerspectiveUrl());
                }

              }, new Command() {
                public void execute() {
                  openPerspectiveOrView(url);
                }
              }, new PerspectiveManager.PerspectiveInitializer() {

                public void onInitialize(Perspective persp) {
                  // open ALL views from perspective
                  List<ViewUrl> viewUrls = config.getAutoOpenViews(url.asPerspectiveUrl());
                  for (ViewUrl viewUrl : viewUrls) {

                    PositionCalculator pc = config.getDefaultPosition(viewUrl);
                    SizeCalculator fsize = config.getDefaultSize(viewUrl);

                    try {
                      openOrActivateView(persp, viewUrl, pc, fsize, true);
                    } catch (BadUrlException e) {
                      e.printStackTrace();
                    }
                  }
                }
              }, helpManager

          );

          desktop.getMenuTree().highlight(url);
          helpManager.setActiveAuthor(url);

          updateMaximizedFrames(perspective);
        } catch (BadUrlException e) {
          e.printStackTrace();
        }

      }
    }

  }

  private void updateMaximizedFrames(Perspective perspective) {
    TabbedWorkPanel.Tab panel = perspectiveManager.getOpenedPerspectiveTab(perspective
        .getPerspectiveUrl());
    if (panel != null) {
      maxFrameResizeManager.setActivePanel(panel);
    }
  }

  private void openOrActivateView(Perspective perspective, final ViewUrl viewUrl,
      PositionCalculator pos, SizeCalculator size, boolean neverMaximize) throws BadUrlException {
    viewManager.openView(viewUrl, perspectiveManager.getOpenedPerspectivePanel(perspective
        .getPerspectiveUrl()), new FrameActivationListener() {

      public void onActivation() {
        openPerspectiveOrView(viewUrl);

      }

    }, new FrameButtonPressedListener() {

      public void onCloseButtonPressed() {
        closeView(viewUrl);

      }

      @Override
      public void onMaximizeButtonPressed() {
        viewManager.maximizeView(viewUrl);
      }

      @Override
      public void onMinimizeButtonPressed() {
        viewManager.minimizeView(viewUrl);

      }

    }, pos, size, helpManager, neverMaximize);
  }

  private boolean checkUserPermissions(String historyToken) {
    if (guard != null) {
      return guard.isAllowed(historyToken);
    } else {
      return true;
    }
  }

  /**
   * It opens perspective or view addressed by url.
   * 
   * If it is perspective: It opens perspective and all its default views. If it
   * already opened, just makes it active. If it does not exist � does nothing.
   * Highlights corresponding point in navigation. Set corresponding text in
   * help panel.
   * 
   * If it is view: Opens (if not yet opened) or activate (if opened)
   * perspective, where this view located. Opens or activates view. If it does
   * not exist � does nothing. Highlights corresponding point in navigation. Set
   * corresponding text in help panel.
   * 
   * @param url
   */
  public void openPerspectiveOrView(ModelUrl url) {

    if (url == null)
      return;

    // Using perspectiveId it changes URL in browser via History API
    // It automatically leads to call onValueChange
    String currentToken = History.getToken();
    String newToken = url.toString();

    if (newToken.equals(currentToken)) {
      // This case is necessary, because if the URL is the same,
      // History.newItem will NOT raise an event
      // But the same URL does not mean - the same content
      // TODO - check it again if it really needed
      onHistoryChanged(newToken);
    } else {
      History.newItem(newToken);
    }

  }

  /**
   * Closes view by given url. If no such view exists, does not close it.
   * Activates perspective this view belonged. Does not close view if
   * ViewCloseListener prevents it
   * 
   * @param viewUrl
   */
  public void closeView(final ViewUrl viewUrl) {

    viewManager.tryClose(viewUrl, new CloseConfirmation() {

      public void confirmClose() {
        viewManager.closeView(viewUrl);
        openPerspectiveOrView(viewUrl.getPerspectiveUrl());

      }

    });

  }

  public Set<PerspectiveUrl> getOpenedPerspecives() {
    return perspectiveManager.getOpenedPerspectives();
  }

  public AbsolutePanel getOpenedPerspectivePanel(PerspectiveUrl url) {
    return perspectiveManager.getOpenedPerspectivePanel(url);
  }

  /**
   * It closes perspective with all its views. Activates the first perspective
   * (if exists)
   * 
   * If no perspective with this URL exists, does nothing.
   * 
   * If any of view inside perspective, restricts closing � does not close
   * perspective and all its views.
   * 
   * @param perspectiveUrl
   */
  public void closePerspective(final PerspectiveUrl perspectiveUrl) {
    // Check if we may close all views from this perspective
    final Set<ViewUrl> tobeclosed = new HashSet<ViewUrl>();

    ensureViewsClosability(perspectiveUrl, tobeclosed, new CloseConfirmation() {

      public void confirmClose() {
        // Close all views with viewManager
        for (ViewUrl viewUrl : tobeclosed) {
          viewManager.closeView(viewUrl);
        }

        // close perspective with PerspectiveManager

        perspectiveManager.closePerspectiveTab(perspectiveUrl);

        // Set the first perspective as active

        Set<PerspectiveUrl> openedPerspecives = perspectiveManager.getOpenedPerspectives();
        if (openedPerspecives.size() > 0) {

          openPerspectiveOrView(openedPerspecives.iterator().next());
        } else {
          // If no perspective left
          History.newItem("");
        }

      }

    });

  }

  /**
   * Checks each view in perspective - if it can be closed. If ALL can be closed
   * - call callback. Stops executing on first non closable view.
   */
  private void ensureViewsClosability(final PerspectiveUrl perspectiveUrl,
      final Set<ViewUrl> tobeclosed, final CloseConfirmation callback) {

    final ViewUrl url = getFirstOpenedView(perspectiveUrl, tobeclosed);

    if (url != null) {
      // We have at least one opened View yet
      // Try close it
      viewManager.tryClose(url, new CloseConfirmation() {

        public void confirmClose() {
          tobeclosed.add(url);
          ensureViewsClosability(perspectiveUrl, tobeclosed, callback);
        }

      });
    } else {
      callback.confirmClose();
    }
  }

  private ViewUrl getFirstOpenedView(PerspectiveUrl perspectiveUrl, Set<ViewUrl> tobeclosed) {

    Perspective perspective = perspectiveManager.getOpenedPerspective(perspectiveUrl);

    if (perspective == null) {
      // perspective is already closed
      return null;
    }

    Set<ViewUrl> allViews = viewManager.getOpenedViews(perspectiveManager
        .getOpenedPerspectivePanel(perspective.getPerspectiveUrl()));

    for (ViewUrl viewUrl : allViews) {
      if (!tobeclosed.contains(viewUrl)) {
        // it is not to be closed
        return viewUrl;
      }
    }
    // all views are either closed or to be closed
    return null;
  }

  /**
   * If you do not see it, this super-method returns desktop :-)
   * 
   * @return desktop
   */
  public Desktop getDesktop() {
    return desktop;
  }

}
